package com.gitee.wsl.io.channel.read.ext

import com.gitee.wsl.annotation.InternalAPI
import com.gitee.wsl.io.BytePacketBuilder
import com.gitee.wsl.io.buildPacket
import com.gitee.wsl.io.channel.CHANNEL_MAX_SIZE
import com.gitee.wsl.io.channel.read.ByteReadChannel
import com.gitee.wsl.io.channel.write.ByteWriteChannel
import com.gitee.wsl.io.channel.write.ext.writeByte
import com.gitee.wsl.io.channel.write.ext.writeFully
import com.gitee.wsl.io.discard
import com.gitee.wsl.io.readAvailable
import com.gitee.wsl.io.remaining
import com.gitee.wsl.io.size
import com.gitee.wsl.io.writePacket
import kotlinx.coroutines.yield
import kotlinx.io.Buffer
import kotlinx.io.EOFException
import kotlinx.io.IOException
import kotlinx.io.InternalIoApi
import kotlinx.io.Source
import kotlinx.io.UnsafeIoApi
import kotlinx.io.bytestring.ByteString
import kotlinx.io.bytestring.decodeToString
import kotlinx.io.indexOf
import kotlinx.io.readByteArray
import kotlinx.io.readByteString
import kotlinx.io.readString
import kotlinx.io.readTo
import kotlinx.io.unsafe.UnsafeBufferOperations
import kotlin.math.min


@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readByte(): Byte {
    if (readBuffer.exhausted()) {
        awaitContent()
    }

    if (readBuffer.exhausted()) {
        throw EOFException("Not enough data available")
    }

    return readBuffer.readByte()
}

@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readShort(): Short {
    awaitUntilReadable(Short.SIZE_BYTES)
    return readBuffer.readShort()
}

@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readInt(): Int {
    awaitUntilReadable(Int.SIZE_BYTES)
    return readBuffer.readInt()
}

@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readLong(): Long {
    awaitUntilReadable(Long.SIZE_BYTES)
    return readBuffer.readLong()
}

private suspend fun ByteReadChannel.awaitUntilReadable(numberOfBytes: Int) {
    while (availableForRead < numberOfBytes && awaitContent(numberOfBytes)) {
        yield()
    }

    if (availableForRead < numberOfBytes) throw EOFException("Not enough data available")
}

@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readBuffer(): Buffer {
    val result = Buffer()
    while (!isClosedForRead) {
        result.transferFrom(readBuffer)
        awaitContent()
    }

    closedCause?.let { throw it }

    return result
}

@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readBuffer(max: Int): Buffer {
    val result = Buffer()
    var remaining = max

    while (remaining > 0 && !isClosedForRead) {
        if (readBuffer.exhausted()) awaitContent()

        val size = minOf(remaining.toLong(), readBuffer.remaining)
        readBuffer.readTo(result, size)
        remaining -= size.toInt()
    }

    return result
}

/**
 * Reads a line of UTF-8 characters from the `ByteReadChannel`.
 * It recognizes CR, LF and CRLF as line delimiters.
 *
 * @param max the maximum number of characters to read. Default is [Int.MAX_VALUE].
 * @return a string containing the line read, or null if channel is closed
 * @throws TooLongLineException if max is reached before encountering a newline or end of input
 */
 suspend fun ByteReadChannel.readUTF8Line(max: Int = Int.MAX_VALUE): String? {
    val result = StringBuilder()
    val completed = readUTF8LineTo(result, max)
    return if (!completed) null else result.toString()
}

 suspend fun ByteReadChannel.readByteArray(count: Int): ByteArray = buildPacket {
    while (size < count) {
        val packet = readPacket(count - size)
        writePacket(packet)
    }
}.readByteArray()

@OptIn(InternalAPI::class, InternalIoApi::class)
 suspend fun ByteReadChannel.readRemaining(): Source {
    val result = BytePacketBuilder()
    while (!isClosedForRead) {
        result.transferFrom(readBuffer)
        awaitContent()
    }

    rethrowCloseCauseIfNeeded()
    return result.buffer
}

@OptIn(InternalAPI::class, InternalIoApi::class)
 suspend fun ByteReadChannel.readRemaining(max: Long): Source {
    val result = BytePacketBuilder()
    var remaining = max
    while (!isClosedForRead && remaining > 0) {
        if (remaining >= readBuffer.remaining) {
            remaining -= readBuffer.remaining
            readBuffer.transferTo(result)
        } else {
            readBuffer.readTo(result, remaining)
            remaining = 0
        }

        awaitContent()
    }

    return result.buffer
}

/**
 * Reads all available bytes to [dst] buffer and returns immediately or suspends if no bytes available
 * @return number of bytes were read or `-1` if the channel has been closed
 */
@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readAvailable(
    buffer: ByteArray,
    offset: Int = 0,
    length: Int = buffer.size - offset
): Int {
    if (isClosedForRead) return -1
    if (readBuffer.exhausted()) awaitContent()
    if (isClosedForRead) return -1

    return readBuffer.readAvailable(buffer, offset, length)
}

/**
 * Invokes [block] if it is possible to read at least [min] byte
 * providing buffer to it so lambda can read from the buffer
 * up to [Buffer.readRemaining] bytes. If there are no [min] bytes available then the invocation returns -1.
 *
 * Warning: it is not guaranteed that all of available bytes will be represented as a single byte buffer
 * eg: it could be 4 bytes available for read but the provided byte buffer could have only 2 available bytes:
 * in this case you have to invoke read again (with decreased [min] accordingly).
 *
 * @param min amount of bytes available for read, should be positive
 * @param block to be invoked when at least [min] bytes available
 *
 * @return number of consumed bytes or -1 if the block wasn't executed.
 */
@OptIn(InternalAPI::class, InternalIoApi::class)
 fun ByteReadChannel.readAvailable(min: Int, block: (Buffer) -> Int): Int {
    require(min > 0) { "min should be positive" }
    require(min <= CHANNEL_MAX_SIZE) { "Min($min) shouldn't be greater than $CHANNEL_MAX_SIZE" }

    if (availableForRead < min) return -1
    return block(readBuffer.buffer)
}

/**
 * Reads a packet of [packet] bytes from the channel.
 *
 * @throws EOFException if the channel is closed before the packet is fully read.
 */
@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readPacket(packet: Int): Source {
    val result = Buffer()
    while (result.size < packet) {
        if (readBuffer.exhausted()) awaitContent()
        if (isClosedForRead) break

        if (readBuffer.remaining > packet - result.size) {
            readBuffer.readTo(result, packet - result.size)
        } else {
            readBuffer.transferTo(result)
        }
    }

    if (result.size < packet) {
        throw EOFException("Not enough data available, required $packet bytes but only ${result.size} available")
    }
    return result
}


/**
 * Reads a line of UTF-8 characters to the specified [out] buffer.
 * It recognizes CR, LF and CRLF as a line delimiter.
 *
 * @param out the buffer to write the line to
 * @param max the maximum number of characters to read
 *
 * @return `true` if a new line separator was found or max bytes appended. `false` if no new line separator and no bytes read.
 * @throws TooLongLineException if max is reached before encountering a newline or end of input
 */
@OptIn(InternalAPI::class, InternalIoApi::class)
 suspend fun ByteReadChannel.readUTF8LineTo(out: Appendable, max: Int = Int.MAX_VALUE): Boolean {
    if (readBuffer.exhausted()) awaitContent()
    if (isClosedForRead) return false

    var consumed = 0
    while (!isClosedForRead) {
        awaitContent()

        val cr = readBuffer.indexOf('\r'.code.toByte())
        val lf = readBuffer.indexOf('\n'.code.toByte())

        // No new line separator
        if (cr == -1L && lf == -1L) {
            if (max == Int.MAX_VALUE) {
                val value = readBuffer.readString()
                out.append(value)
            } else {
                val count = minOf(max - consumed, readBuffer.remaining.toInt())
                consumed += count
                out.append(readBuffer.readString(count.toLong()))

                if (consumed == max) throw TooLongLineException("Line exceeds limit of $max characters")
            }

            continue
        }

        // CRLF fully in buffer
        if (cr >= 0 && lf == cr + 1) {
            val count = if (max != Int.MAX_VALUE) cr else minOf(max - consumed, cr.toInt()).toLong()
            out.append(readBuffer.readString(count))
            if (count == cr) readBuffer.discard(2)
            return true
        }

        // CR in buffer before LF
        if (cr >= 0 && (lf == -1L || cr < lf)) {
            val count = if (max != Int.MAX_VALUE) cr else minOf(max - consumed, cr.toInt()).toLong()
            out.append(readBuffer.readString(count))
            if (count == cr) readBuffer.discard(1)

            // Check if LF follows CR after awaiting
            if (readBuffer.exhausted()) awaitContent()
            if (readBuffer.buffer[0] == '\n'.code.toByte()) {
                readBuffer.discard(1)
            }

            return true
        }

        // LF in buffer before CR
        if (lf >= 0) {
            val count = if (max != Int.MAX_VALUE) lf else minOf(max - consumed, lf.toInt()).toLong()
            out.append(readBuffer.readString(count))
            if (count == lf) readBuffer.discard(1)
            return true
        }
    }

    return true
}

@OptIn(InternalAPI::class, UnsafeIoApi::class, InternalIoApi::class)
 suspend inline fun ByteReadChannel.read(crossinline block: suspend (ByteArray, Int, Int) -> Int): Int {
    if (isClosedForRead) return -1
    if (readBuffer.exhausted()) awaitContent()
    if (isClosedForRead) return -1

    var result: Int
    UnsafeBufferOperations.readFromHead(readBuffer.buffer) { array, start, endExclusive ->
        result = block(array, start, endExclusive)
        result
    }

    return result
}



/**
 * Reads bytes from [start] to [end] into the provided [out] buffer, or fails
 * if the channel has been closed.
 *
 * Suspension occurs when there are not enough bytes available in the channel.
 *
 * @param out the buffer to write to
 * @param start the index to start writing at
 * @param end the index to write until
 */
@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.readFully(out: ByteArray, start: Int = 0, end: Int = out.size) {
    if (isClosedForRead) throw EOFException("Channel is already closed")

    var offset = start
    while (offset < end) {
        if (readBuffer.exhausted()) awaitContent()
        if (isClosedForRead) throw EOFException("Channel is already closed")

        val count = min(end - offset, readBuffer.remaining.toInt())
        readBuffer.readTo(out, offset, offset + count)
        offset += count
    }
}


/**
 * Reads bytes from the ByteReadChannel until a specified sequence of bytes is encountered or the specified limit is reached.
 *
 * This uses the KMP algorithm for finding the string match using a partial match table.
 *
 * @see [Knuth–Morris–Pratt algorithm](https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm)
 * @param matchString The sequence of bytes to look for.
 * @param writeChannel The channel to write the read bytes to.
 * @param limit The maximum number of bytes to read before throwing an exception.
 * @param ignoreMissing Whether to ignore the missing byteString and return the count of read bytes upon reaching the end of input.
 * @return The number of bytes read, not including the search string.
 * @throws IOException If the limit is exceeded or the byteString is not found and ignoreMissing is false.
 */
 suspend fun ByteReadChannel.readUntil(
    matchString: ByteString,
    writeChannel: ByteWriteChannel,
    limit: Long = Long.MAX_VALUE,
    ignoreMissing: Boolean = false,
): Long {
    check(matchString.size > 0) {
        "Empty match string not permitted for readUntil"
    }
    val partialMatchTable = buildPartialMatchTable(matchString)
    var matchIndex = 0
    val matchBuffer = ByteArray(matchString.size)
    var rc = 0L

    suspend fun appendPartialMatch() {
        writeChannel.writeFully(matchBuffer, 0, matchIndex)
        rc += matchIndex
        matchIndex = 0
    }

    fun resetPartialMatch(byte: Byte) {
        while (matchIndex > 0 && byte != matchString[matchIndex]) {
            matchIndex = partialMatchTable[matchIndex - 1]
        }
    }

    while (!isClosedForRead) {
        val byte = readByte()

        if (matchIndex > 0 && byte != matchString[matchIndex]) {
            appendPartialMatch()
            resetPartialMatch(byte)
        }

        if (byte == matchString[matchIndex]) {
            matchBuffer[matchIndex] = byte
            if (++matchIndex == matchString.size) {
                return rc
            }
        } else {
            writeChannel.writeByte(byte)
            rc++
        }

        if (rc > limit) {
            throw IOException("Limit of $limit bytes exceeded while scanning for \"${matchString.decodeToString()}\"")
        }
    }

    if (ignoreMissing) {
        appendPartialMatch()
        writeChannel.flush()
        return rc
    }

    throw IOException("Expected \"${matchString.toSingleLineString()}\" but encountered end of input")
}

/**
 * Helper function to build the partial match table (also known as "longest prefix suffix" table)
 */
private fun buildPartialMatchTable(byteString: ByteString): IntArray {
    val table = IntArray(byteString.size)
    var j = 0

    for (i in 1 until byteString.size) {
        while (j > 0 && byteString[i] != byteString[j]) {
            j = table[j - 1]
        }
        if (byteString[i] == byteString[j]) {
            j++
        }
        table[i] = j
    }

    return table
}

// Used in formatting errors
private fun ByteString.toSingleLineString() =
    decodeToString().replace("\n", "\\n")


/**
 * Retrieves, but does not consume, up to the specified number of bytes from the current position in this
 * [ByteReadChannel].
 *
 * @param count The number of bytes to peek.
 * @return A [ByteString] containing the bytes that were peeked, or null if unable to peek the specified number of bytes.
 */
@OptIn(InternalAPI::class)
 suspend fun ByteReadChannel.peek(count: Int): ByteString? {
    if (isClosedForRead) return null
    if (!awaitContent(count)) return null
    return readBuffer.peek().readByteString(count)
}

 class TooLongLineException(message: String) : IOException(message)

suspend fun ByteReadChannel.readMedium(): Int {
    return (readByte().toInt() shl 16) + (readByte().toInt() shl 8) + readByte().toInt()
}

suspend fun ByteReadChannel.readUByte(): Int {
    return readByte().toInt() and 0xff
}

suspend fun ByteReadChannel.readUMedium(): Int {
    return (readUByte() shl 16) + (readUByte() shl 8) + readUByte()
}